/*
 * Copyright 2006-2007 S W McIlroy
 * Licensed  under the  Apache License,  Version 2.0  (the "License");
 * you may not use  this file  except in  compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed  under the  License is distributed on an "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mockfu;

import static org.mockfu.Exceptions.*;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;

import org.mockfu.MockControl.MockInterceptor;

public class Injection
{
    private static Set<Injection> injections = new LinkedHashSet<Injection>();

    private final Field field;
    private final Object unit;
    private final Object originalValue;
    private final Object injectedValue;
    private final MockControl<?> control;

    public static void ejectAll()
    {
        synchronized (injections)
        {
            for (Injection injection : new ArrayList<Injection>(injections))
                injection.eject();
        }
    }

    private static String getFieldName(Class<?> klass, Object injectedValue)
    {
        Field matched = null;
        for (Field field : klass.getDeclaredFields())
        {
            if ((field.getModifiers() & Modifier.STATIC) == 0
                    && field.getType().isAssignableFrom(injectedValue.getClass()))
            {
                if (matched != null)
                {
                    throw undeclared(new NoSuchFieldException("Cannot inject mock by type"
                            + " because multiple fields are assignable from " + injectedValue.getClass()));
                }
                matched = field;
            }
        }
        if (matched == null)
            throw undeclared(new NoSuchFieldException(""));

        return matched.getName();
    }

    public Injection(Object unit, Object dependency)
    {
        this(null, unit, getFieldName(unit.getClass(), dependency), dependency);
    }

    public Injection(Object unit, String fieldName, Object dependency)
    {
        this(null, unit, fieldName, dependency);
    }

    public Injection(Class<?> klass, Object dependency)
    {
        this(klass, null, getFieldName(klass, dependency), dependency);
    }

    public Injection(Class<?> klass, String fieldName, Object dependency)
    {
        this(klass, null, fieldName, dependency);
    }

    private Injection(Class<?> klass, Object unit, String fieldName, Object dependency)
    {
        if (klass == null)
            klass = unit.getClass();

        this.unit = (unit != null) ? unit : null;
        this.injectedValue = dependency;
        if (injectedValue instanceof Proxy && ((Proxy)injectedValue).$getInterceptor() instanceof MockInterceptor)
            this.control = ((MockInterceptor)((Proxy)injectedValue).$getInterceptor()).getControl();
        else
            this.control = null;

        try
        {
            this.field = klass.getDeclaredField( fieldName );
            boolean accessible = field.isAccessible();
            field.setAccessible( true );
            try
            {
                this.originalValue = field.get( unit );
            }
            finally
            {
                field.setAccessible( accessible );
            }
        }
        catch( Exception ex )
        {
            throw undeclared( ex );
        }
    }

    public void inject()
    {
        synchronized (injections)
        {
            try
            {
                /* if there's an existing mock injection, use it to demux */
                for (Injection injection : injections)
                {
                    if (injection.equals(this) && injection.injectedValue instanceof Proxy)
                    {
                        MethodInterceptor interceptor = ((Proxy)injection.injectedValue).$getInterceptor();
                        if (interceptor instanceof MockInterceptor)
                        {
                            MockInterceptor mockInterceptor = (MockInterceptor)interceptor;
                            /* set the injection for this (and child) threads */
                            mockInterceptor.setInjection(this);
                            return;
                        }
                    }
                }

                /* inject field thru reflection */
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                try
                {
                    if (injectedValue instanceof Proxy)
                    {
                        MethodInterceptor interceptor = ((Proxy)injectedValue).$getInterceptor();
                        if (interceptor instanceof MockInterceptor)
                        {
                            ((MockInterceptor)interceptor).setInjection(this);
                        }
                    }
                    field.set(unit, injectedValue);
                    injections.add(this);
                }
                finally
                {
                    field.setAccessible(accessible);
                }
            }
            catch (Exception ex)
            {
                /* injections are usually done in a setUp method where exceptions can be masked by
                 * tearDown exceptions ... log the exception so the user knows about it */
                ex.printStackTrace();
                System.err.flush();
                throw undeclared(ex);
            }
        }
    }

    public void eject()
    {
        synchronized (injections)
        {
            /* if injection was demuxed, remove it from demuxer */
            for (Injection injection : injections)
            {
                if (injection.equals(this) && injection.injectedValue instanceof Proxy)
                {
                    MethodInterceptor interceptor = ((Proxy)injection.injectedValue).$getInterceptor();
                    if (interceptor instanceof MockInterceptor)
                    {
                        MockInterceptor mockInterceptor = (MockInterceptor)interceptor;
                        mockInterceptor.removeInjection();
                        if (mockInterceptor.isFieldEjectable())
                            /* eject field */
                            break;
                        return;
                    }
                }
            }

            /* eject field thru reflection */
            try
            {
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                try
                {
                    field.set(unit, originalValue);
                }
                finally
                {
                    field.setAccessible(accessible);
                    injections.remove(this);
                }
            }
            catch (Exception ex)
            {
                throw undeclared(ex);
            }
        }
    }

    Object invokeOriginal(InvocationContext context)
    {
        if (originalValue == null)
            throw new IllegalStateException("Expired mock found in field " + this.field);
        try
        {
            return context.getMethod().invoke(originalValue, context.getArguments());
        }
        catch (Exception ex)
        {
            throw undeclared(ex);
        }
    }

    MockControl<?> getControl()
    {
        return control;
    }

    @Override
    public String toString()
    {
        return field.toString();
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((field == null) ? 0 : field.hashCode());
        result = prime * result + ((unit == null) ? 0 : System.identityHashCode(unit));
        return result;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Injection other = (Injection)obj;
        if (field == null)
        {
            if (other.field != null)
                return false;
        }
        else if (!field.equals(other.field))
            return false;
        if (unit == null)
        {
            if (other.unit != null)
                return false;
        }
        else if (unit != other.unit)
            return false;
        return true;
    }
}
